Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various memory optimizations #94

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Conversation

hack3ric
Copy link

@hack3ric hack3ric commented Nov 15, 2024

See each commit message for details.

These are a lot of breaking changes, so please take your time to check if they are properly done :)

@hack3ric
Copy link
Author

hack3ric commented Nov 15, 2024

Oops, this failed to build on Rust <1.79 because the compiler cannot decide if [Expression]: ToOwned:

error[E0277]: the trait bound `[SetItem]: ToOwned` is not satisfied in `Expression`
   --> src/stmt.rs:245:14
    |
245 |     pub dev: Option<Expression>,
    |              ^^^^^^^^^^^^^^^^^^ within `Expression`, the trait `ToOwned` is not implemented for `[SetItem]`, which is required by `Expression: Sized`
    |
    = help: the trait `ToOwned` is implemented for `[T]`
note: required because it appears within the type `Expression`
   --> src/expr.rs:10:10
    |
10  | pub enum Expression {
    |          ^^^^^^^^^^
note: required by an implicit `Sized` bound in `std::option::Option`
   --> /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/option.rs:572:1

...and many, many similar errors

If bumping MSRV to version this recent is not acceptable, keeping Vecs would probably fix this issue.

A lot of strings and arrays used in nftables structures are static, but
currently we need to do allocations everywhere using owned String and Vec.
Replace them with Cow so we can pass &'static str or &'static [_] whenever
possible, while returning owned value when deserializing.
This evens out some of the large enums' sizes (e.g. Expression and Statement's)
from over 200 bytes to <=144. Further reduction could be done, as this commit
only changes the obvious ones for now.

Also moves BinaryOperation's Box up one level so it only uses one Box.
Since we only expect two elements from Range, we can just use array instead.
This fails to build on 1.65:

```
error[E0277]: the trait bound `[SetItem]: ToOwned` is not satisfied in `Expression`
   --> src/stmt.rs:245:14
    |
245 |     pub dev: Option<Expression>,
    |              ^^^^^^^^^^^^^^^^^^ within `Expression`, the trait `ToOwned` is not implemented for `[SetItem]`, which is required by `Expression: Sized`
    |
    = help: the trait `ToOwned` is implemented for `[T]`
note: required because it appears within the type `Expression`
   --> src/expr.rs:10:10
    |
10  | pub enum Expression {
    |          ^^^^^^^^^^
note: required by an implicit `Sized` bound in `std::option::Option`
   --> /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/option.rs:572:1

...and many, many similar errors
```

It compiles successfully on Rust >=1.79, so this commit can be reverted once our MSRV reaches it.
@hack3ric
Copy link
Author

Fixed MSRV build and clippy. Please have another look :)

@JKRhb JKRhb requested a review from jwhb November 24, 2024 16:09
@jwhb
Copy link
Member

jwhb commented Nov 24, 2024

Amazing @hack3ric!

Please allow us some more days to review it.

@jwhb jwhb added the enhancement New feature or request label Nov 24, 2024
@jwhb jwhb added this to the v0.6.0 milestone Nov 24, 2024
Copy link
Member

@JKRhb JKRhb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @hack3ric, these changes look very good to me :) As I am personally not that familiar with Cow, see only one question regarding its usage in the tests below.

Comment on lines +86 to +87
table: Cow::Borrowed("some_inet_table"),
chain: Cow::Borrowed("some_inet_chain"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it also be possible to use .into() in places like these?

Copy link
Author

@hack3ric hack3ric Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl<'a> From<&'a str> for Cow<'a, str> (and its fellows) is not const for now so .into() will result in a temporary value (as seen by rustc, even if it is 'static), therefore cannot be borrowed 'static. In this example both the outer slice and the inner str needs to be borrowed 'static, leading to the following error:

error[E0716]: temporary value dropped while borrowed
   --> tests/json_tests.rs:57:33
    |
57  |            objects: Cow::Borrowed(&[
    |   ________________________________-^
    |  |_________________________________|
58  | ||             NfObject::ListObject(NfListObject::Table(Table {
59  | ||                 family: NfFamily::INet,
60  | ||                 name: "some_inet_table".into(),
...   ||
106 | ||             })),
107 | ||         ]),
    | ||         ^
    | ||_________|
    |  |_________creates a temporary value which is freed while still in use
    |            cast requires that borrow lasts for `'static`
108 |        };
    |         - temporary value is freed at the end of this statement

If all of the outer containers are Cow::Owned, we can use .into() inside. Otherwise we probably need to stick to manually use Cow::Borrowed. I've also pushed a commit that allows temporary arrays to be used by replacing 'static with 'a. It will then just need to move the array in Cow::Borrowed(&[..]) to another local variable to allow the usage of .into():

let objects = [
    NfObject::ListObject(NfListObject::Table(Table {
        family: NfFamily::INet,
        name: "some_inet_table".into(), // here
        handle: None,
    })),
    // ...
];
let expected = Nftables {
    objects: objects.into(); // and here
};

@hack3ric
Copy link
Author

Added one commit that lifts 'static restriction on borrowed values, but added a lot of lifetimes, and updated examples in README as well.

This adds a bunch of lifetimes, but it works with `.into()` when a borrowed value's array container is stored in another place, without the need of `Cow::Borrowed`:

```
let objects = [
    NfObject::ListObject(NfListObject::Table(Table {
        family: NfFamily::INet,
        name: "some_inet_table".into(), // here
        handle: None,
    })),
    // ...
];
let expected = Nftables {
    objects: objects.into(); // and here
};
```

However, `Cow::{Borrowed, Owned}` still needs to be used when you want to write the above in one statement.
@hack3ric
Copy link
Author

Clippy issue should be fixed now.

@hack3ric
Copy link
Author

FYI I have another patch ready for upstream, but it depends on this PR: 52902cd. I will submit another PR after this and I hope this too can get into v0.6.0 too :)

@jwhb
Copy link
Member

jwhb commented Dec 10, 2024

I performed a trivial memory benchmark of current main versus main + your PR.

main branch
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s
------------
 Nextest run ID f1af134f-590a-4b95-8e79-b4d43b409ebe with nextest profile: default
    Starting 11 tests across 1 binary
        PASS [   0.027s] nftables::deserialize test_deserialize_json_files::basic.json
        PASS [   0.032s] nftables::deserialize test_deserialize_json_files::counter.json
        PASS [   0.042s] nftables::deserialize test_deserialize_json_files::nftables-init.json
        PASS [   0.045s] nftables::deserialize test_deserialize_json_files::space-keys.json
        PASS [   0.052s] nftables::deserialize test_deserialize_json_files::flow.json
        PASS [   0.057s] nftables::deserialize test_deserialize_json_files::tproxy.json
        PASS [   0.065s] nftables::deserialize test_deserialize_json_files::nat.json
        PASS [   0.072s] nftables::deserialize test_deserialize_json_files::workstation_combined.json
        PASS [   0.080s] nftables::deserialize test_deserialize_json_files::synproxy.json
        PASS [   0.086s] nftables::deserialize test_deserialize_json_files::setmap.json
        PASS [   0.092s] nftables::deserialize test_deserialize_json_files::workstation.json
------------
     Summary [   0.095s] 11 tests run: 11 passed, 0 skipped
        PASS [   0.027s] nftables::deserialize test_deserialize_json_files::basic.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::basic.json ---

running 1 test
Deserializing file: resources/test/json/basic.json
Current physical memory usage: -20480
Current virtual memory usage: -4096
test test_deserialize_json_files::basic.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.032s] nftables::deserialize test_deserialize_json_files::counter.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::counter.json ---

running 1 test
Deserializing file: resources/test/json/counter.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::counter.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.052s] nftables::deserialize test_deserialize_json_files::flow.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::flow.json ---

running 1 test
Deserializing file: resources/test/json/flow.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::flow.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.065s] nftables::deserialize test_deserialize_json_files::nat.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::nat.json ---

running 1 test
Deserializing file: resources/test/json/nat.json
Current physical memory usage: -20480
Current virtual memory usage: -4096
test test_deserialize_json_files::nat.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.042s] nftables::deserialize test_deserialize_json_files::nftables-init.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::nftables-init.json ---

running 1 test
Deserializing file: resources/test/json/nftables-init.json
Current physical memory usage: -24576
Current virtual memory usage: -8192
test test_deserialize_json_files::nftables-init.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s


        PASS [   0.086s] nftables::deserialize test_deserialize_json_files::setmap.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::setmap.json ---

running 1 test
Deserializing file: resources/test/json/setmap.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::setmap.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.045s] nftables::deserialize test_deserialize_json_files::space-keys.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::space-keys.json ---

running 1 test
Deserializing file: resources/test/json/space-keys.json
Current physical memory usage: -24576
Current virtual memory usage: -8192
test test_deserialize_json_files::space-keys.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.080s] nftables::deserialize test_deserialize_json_files::synproxy.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::synproxy.json ---

running 1 test
Deserializing file: resources/test/json/synproxy.json
Current physical memory usage: -20480
Current virtual memory usage: -4096
test test_deserialize_json_files::synproxy.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.057s] nftables::deserialize test_deserialize_json_files::tproxy.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::tproxy.json ---

running 1 test
Deserializing file: resources/test/json/tproxy.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::tproxy.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.092s] nftables::deserialize test_deserialize_json_files::workstation.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::workstation.json ---

running 1 test
Deserializing file: resources/test/json/workstation.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::workstation.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.072s] nftables::deserialize test_deserialize_json_files::workstation_combined.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::workstation_combined.json ---

running 1 test
Deserializing file: resources/test/json/workstation_combined.json
Current physical memory usage: -20480
Current virtual memory usage: -4096
test test_deserialize_json_files::workstation_combined.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
cow branch
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
------------
 Nextest run ID 09889313-078e-450e-bf34-cc2685fe0a80 with nextest profile: default
    Starting 11 tests across 1 binary
        PASS [   0.028s] nftables::deserialize test_deserialize_json_files::basic.json
        PASS [   0.036s] nftables::deserialize test_deserialize_json_files::nftables-init.json
        PASS [   0.039s] nftables::deserialize test_deserialize_json_files::setmap.json
        PASS [   0.045s] nftables::deserialize test_deserialize_json_files::tproxy.json
        PASS [   0.054s] nftables::deserialize test_deserialize_json_files::flow.json
        PASS [   0.061s] nftables::deserialize test_deserialize_json_files::space-keys.json
        PASS [   0.067s] nftables::deserialize test_deserialize_json_files::workstation_combined.json
        PASS [   0.075s] nftables::deserialize test_deserialize_json_files::counter.json
        PASS [   0.081s] nftables::deserialize test_deserialize_json_files::synproxy.json
        PASS [   0.088s] nftables::deserialize test_deserialize_json_files::workstation.json
        PASS [   0.095s] nftables::deserialize test_deserialize_json_files::nat.json
------------
     Summary [   0.096s] 11 tests run: 11 passed, 0 skipped
        PASS [   0.028s] nftables::deserialize test_deserialize_json_files::basic.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::basic.json ---

running 1 test
Deserializing file: resources/test/json/basic.json
Current physical memory usage: -12288
Current virtual memory usage: -4096
test test_deserialize_json_files::basic.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.075s] nftables::deserialize test_deserialize_json_files::counter.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::counter.json ---

running 1 test
Deserializing file: resources/test/json/counter.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::counter.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.054s] nftables::deserialize test_deserialize_json_files::flow.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::flow.json ---

running 1 test
Deserializing file: resources/test/json/flow.json
Current physical memory usage: -12288
Current virtual memory usage: -4096
test test_deserialize_json_files::flow.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.095s] nftables::deserialize test_deserialize_json_files::nat.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::nat.json ---

running 1 test
Deserializing file: resources/test/json/nat.json
Current physical memory usage: -12288
Current virtual memory usage: -4096
test test_deserialize_json_files::nat.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.036s] nftables::deserialize test_deserialize_json_files::nftables-init.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::nftables-init.json ---

running 1 test
Deserializing file: resources/test/json/nftables-init.json
Current physical memory usage: -24576
Current virtual memory usage: -8192
test test_deserialize_json_files::nftables-init.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s


        PASS [   0.039s] nftables::deserialize test_deserialize_json_files::setmap.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::setmap.json ---

running 1 test
Deserializing file: resources/test/json/setmap.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::setmap.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.061s] nftables::deserialize test_deserialize_json_files::space-keys.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::space-keys.json ---

running 1 test
Deserializing file: resources/test/json/space-keys.json
Current physical memory usage: -20480
Current virtual memory usage: -8192
test test_deserialize_json_files::space-keys.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.081s] nftables::deserialize test_deserialize_json_files::synproxy.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::synproxy.json ---

running 1 test
Deserializing file: resources/test/json/synproxy.json
Current physical memory usage: -20480
Current virtual memory usage: -4096
test test_deserialize_json_files::synproxy.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.045s] nftables::deserialize test_deserialize_json_files::tproxy.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::tproxy.json ---

running 1 test
Deserializing file: resources/test/json/tproxy.json
Current physical memory usage: -12288
Current virtual memory usage: -4096
test test_deserialize_json_files::tproxy.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.088s] nftables::deserialize test_deserialize_json_files::workstation.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::workstation.json ---

running 1 test
Deserializing file: resources/test/json/workstation.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::workstation.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


        PASS [   0.067s] nftables::deserialize test_deserialize_json_files::workstation_combined.json

--- STDOUT:              nftables::deserialize test_deserialize_json_files::workstation_combined.json ---

running 1 test
Deserializing file: resources/test/json/workstation_combined.json
Current physical memory usage: -16384
Current virtual memory usage: -4096
test test_deserialize_json_files::workstation_combined.json ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

@@ -560,17 +560,17 @@ pub struct FlowTable {
/// The *devices* are specified as iifname(s) of the input interface(s) of the traffic that should be offloaded.
///
/// Devices are required for both traffic directions.
/// Vec of device names, e.g. `vec!["wg0".to_string(), "wg0".to_string()]`.
pub dev: Option<Vec<String>>,
/// Cow slice of device names, e.g. `vec!["wg0".into(), "wg1".into()].into()`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@jwhb
Copy link
Member

jwhb commented Dec 10, 2024

LGTM!

I am afraid of the impact of the breaking changes for users of this crate, but this is obviously the way forward.

Copy link
Member

@JKRhb JKRhb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @hack3ric!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants